/*
 * Copyright 2004 - 2013 Wayne Grant
 *           2013 - 2021 Kai Kramer
 *
 * This file is part of KeyStore Explorer.
 *
 * KeyStore Explorer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * KeyStore Explorer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with KeyStore Explorer.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kse.gui.dialogs.sign;

import java.awt.Container;
import java.awt.Dialog;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
import java.util.jar.JarFile;

import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSeparator;
import javax.swing.JTextField;
import javax.swing.KeyStroke;

import org.kse.crypto.CryptoException;
import org.kse.crypto.digest.DigestType;
import org.kse.crypto.keypair.KeyPairType;
import org.kse.crypto.signing.JarSigner;
import org.kse.crypto.signing.SignatureType;
import org.kse.gui.CurrentDirectory;
import org.kse.gui.CursorUtil;
import org.kse.gui.FileChooserFactory;
import org.kse.gui.JEscDialog;
import org.kse.gui.MiGUtil;
import org.kse.gui.PlatformUtil;
import org.kse.gui.dialogs.DialogHelper;
import org.kse.gui.error.DError;
import org.kse.gui.error.DProblem;
import org.kse.gui.error.Problem;
import org.kse.utilities.DialogViewer;
import org.kse.utilities.io.FileNameUtil;
import org.kse.utilities.io.IOUtils;
import org.kse.utilities.net.URLs;

import net.miginfocom.swing.MigLayout;

/**
 * Dialog that displays the presents JAR signing options.
 *
 */
public class DSignJar extends JEscDialog {
	private static final long serialVersionUID = -5095469699284737624L;

	private static ResourceBundle res = ResourceBundle.getBundle("org/kse/gui/dialogs/sign/resources");

	private static final String CANCEL_KEY = "CANCEL_KEY";

	private JLabel jlOutputJarFixes;
	private JRadioButton jrbOutputJarFixes;
	private JLabel jlSignDirectly;
	private JRadioButton jrbSignDirectly;

	private JLabel jlInputJar;
	private JTextField jtfInputJar;
	private JButton jbInputJarBrowse;
	private JLabel jlPrefix;
	private JTextField jtfPrefix;
	private JLabel jlSuffix;
	private JTextField jtfSuffix;
	private JLabel jlSignatureName;
	private JTextField jtfSignatureName;
	private JLabel jlSignatureAlgorithm;
	private JComboBox<SignatureType> jcbSignatureAlgorithm;
	private JLabel jlDigestAlgorithm;
	private JComboBox<DigestType> jcbDigestAlgorithm;
	private JLabel jlAddTimestamp;
	private JCheckBox jcbAddTimestamp;
	private JLabel jlTimestampServerUrl;
	private JComboBox<String> jcbTimestampServerUrl;
	private JPanel jpButtons;
	private JButton jbOK;
	private JButton jbCancel;
	private JLabel jlFileCount;

	private PrivateKey signPrivateKey;
	private KeyPairType signKeyPairType;
	private File[] inputJarFiles;
	private List<File> outputJarFiles;
	private String signatureName;
	private SignatureType signatureType;
	private DigestType digestType;
	private String tsaUrl;
	private boolean successStatus = true;

	/**
	 * Creates a new DSignJar dialog.
	 *
	 * @param parent          The parent frame
	 * @param signPrivateKey  Signing key pair's private key
	 * @param signKeyPairType Signing key pair's type
	 * @param signatureName   Default signature name
	 * @throws CryptoException A crypto problem was encountered constructing the
	 *                         dialog
	 */
	public DSignJar(JFrame parent, PrivateKey signPrivateKey, KeyPairType signKeyPairType, String signatureName)
			throws CryptoException {
		super(parent, Dialog.ModalityType.DOCUMENT_MODAL);
		this.signPrivateKey = signPrivateKey;
		this.signKeyPairType = signKeyPairType;
		setTitle(res.getString("DSignJar.Title"));
		initComponents(signatureName);
	}

	/**
	 * Initializes the dialogue panel and associated elements
	 * 
	 * @param signatureName String
	 * @throws CryptoException
	 */
	private void initComponents(String signatureName) throws CryptoException {
		jlSignDirectly = new JLabel(res.getString("DSignJar.jlSignDirectly.text"));
		jrbSignDirectly = new JRadioButton("", true);
		jrbSignDirectly.setToolTipText(res.getString("DSignJar.jrbSignDirectly.tooltip"));

		jlOutputJarFixes = new JLabel(res.getString("DSignJar.jlOutputJarFixes.text"));
		jrbOutputJarFixes = new JRadioButton("", false);

		ButtonGroup buttonGroup = new ButtonGroup();
		buttonGroup.add(jrbSignDirectly);
		buttonGroup.add(jrbOutputJarFixes);

		jlInputJar = new JLabel(res.getString("DSignJar.jlInputJar.text"));
		jtfInputJar = new JTextField(30);
		jtfInputJar.setCaretPosition(0);
		jtfInputJar.setToolTipText(res.getString("DSignJar.jtfInputJar.tooltip"));
		
		jlFileCount = new JLabel();
		String message = MessageFormat.format(res.getString("DSignJar.jlFileCount.text"), 0);
		jlFileCount.setText(message);

		jbInputJarBrowse = new JButton(res.getString("DSignJar.jbInputJarBrowse.text"));
		PlatformUtil.setMnemonic(jbInputJarBrowse, res.getString("DSignJar.jbInputJarBrowse.mnemonic").charAt(0));
		jbInputJarBrowse.setToolTipText(res.getString("DSignJar.jbInputJarBrowse.tooltip"));

		jlPrefix = new JLabel(res.getString("DSignJar.jlPrefix.text"));
		jtfPrefix = new JTextField("", 15);
		jtfPrefix.setEnabled(false);
		jtfPrefix.setCaretPosition(0);
		jtfPrefix.setToolTipText(res.getString("DSignJar.jtfPrefix.tooltip"));

		jlSuffix = new JLabel(res.getString("DSignJar.jlSuffix.text"));
		jtfSuffix = new JTextField("", 15);
		jtfSuffix.setEnabled(false);
		jtfSuffix.setCaretPosition(0);
		jtfSuffix.setToolTipText(res.getString("DSignJar.jtfSuffix.tooltip"));

		jlSignatureName = new JLabel(res.getString("DSignJar.jlSignatureName.text"));
		jtfSignatureName = new JTextField(convertSignatureName(signatureName), 15);
		jtfSignatureName.setCaretPosition(0);
		jtfSignatureName.setToolTipText(res.getString("DSignJar.jtfSignatureName.tooltip"));

		jlSignatureAlgorithm = new JLabel(res.getString("DSignJar.jlSignatureAlgorithm.text"));
		jcbSignatureAlgorithm = new JComboBox<>();
		DialogHelper.populateSigAlgs(signKeyPairType, this.signPrivateKey, jcbSignatureAlgorithm);
		jcbSignatureAlgorithm.setToolTipText(res.getString("DSignJar.jcbSignatureAlgorithm.tooltip"));

		jlDigestAlgorithm = new JLabel(res.getString("DSignJar.jlDigestAlgorithm.text"));
		jcbDigestAlgorithm = new JComboBox<>();
		populateDigestAlgs();
		jcbDigestAlgorithm.setToolTipText(res.getString("DSignJar.jcbDigestAlgorithm.tooltip"));

		jlAddTimestamp = new JLabel(res.getString("DSignJar.jlAddTimestamp.text"));
		jcbAddTimestamp = new JCheckBox();
		jcbAddTimestamp.setSelected(false);
		jcbAddTimestamp.setToolTipText(res.getString("DSignJar.jcbAddTimestamp.tooltip"));

		jlTimestampServerUrl = new JLabel(res.getString("DSignJar.jlTimestampServerUrl.text"));
		jcbTimestampServerUrl = new JComboBox<>();
		jcbTimestampServerUrl.setEditable(true);
		jcbTimestampServerUrl.setEnabled(false);
		jcbTimestampServerUrl.setToolTipText(res.getString("DSignJar.jcbTimestampServerUrl.tooltip"));
		jcbTimestampServerUrl.setModel(new DefaultComboBoxModel<>(URLs.TSA_URLS));

		jbOK = new JButton(res.getString("DSignJar.jbOK.text"));

		jbCancel = new JButton(res.getString("DSignJar.jbCancel.text"));
		jbCancel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
				CANCEL_KEY);

		jpButtons = PlatformUtil.createDialogButtonPanel(jbOK, jbCancel);

		// layout
		Container pane = getContentPane();
		pane.setLayout(new MigLayout("insets dialog, fill", "[para]unrel[right]unrel[]", "[]unrel[]"));
		MiGUtil.addSeparator(pane, res.getString("DSignJar.jlFiles.text"));
		pane.add(jlInputJar, "skip");
		pane.add(jbInputJarBrowse, "flowx");
		pane.add(jlFileCount, "cell 2 1, wrap");
		pane.add(jlSignDirectly, "skip");
		pane.add(jrbSignDirectly, "wrap");
		pane.add(jlOutputJarFixes, "skip");
		pane.add(jrbOutputJarFixes, "wrap");
		pane.add(jlPrefix, "skip");
		pane.add(jtfPrefix, "wrap");
		pane.add(jlSuffix, "skip");
		pane.add(jtfSuffix, "wrap");
		MiGUtil.addSeparator(pane, res.getString("DSignJar.jlSignature.text"));
		pane.add(jlSignatureName, "skip");
		pane.add(jtfSignatureName, "sgx, wrap");
		pane.add(jlSignatureAlgorithm, "skip");
		pane.add(jcbSignatureAlgorithm, "sgx, wrap");
		pane.add(jlDigestAlgorithm, "skip");
		pane.add(jcbDigestAlgorithm, "sgx, wrap para");
		MiGUtil.addSeparator(pane, res.getString("DSignJar.jlTimestamp.text"));
		pane.add(jlAddTimestamp, "skip");
		pane.add(jcbAddTimestamp, "wrap");
		pane.add(jlTimestampServerUrl, "skip");
		pane.add(jcbTimestampServerUrl, "sgx, wrap para");
		pane.add(new JSeparator(), "spanx, growx, wrap para");
		pane.add(jpButtons, "right, spanx");

		// actions
		jbInputJarBrowse.addActionListener(evt -> {
			try {
				CursorUtil.setCursorBusy(DSignJar.this);
				inputJarBrowsePressed();
			} finally {
				CursorUtil.setCursorFree(DSignJar.this);
			}
		});

		jcbAddTimestamp.addItemListener(evt -> enableDisableElements());
		jrbSignDirectly.addItemListener(evt -> enableDisableElements());
		jrbOutputJarFixes.addItemListener(evt -> enableDisableElements());

		jbOK.addActionListener(evt -> okPressed());

		jbCancel.getActionMap().put(CANCEL_KEY, new AbstractAction() {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent evt) {
				cancelPressed();
			}
		});
		jbCancel.addActionListener(evt -> cancelPressed());

		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent evt) {
				closeDialog();
			}
		});

		setResizable(true);

		getRootPane().setDefaultButton(jbOK);

		pack();
		setLocationRelativeTo(null);

	}

	/**
	 * This function enables and disables elements in the dialog
	 */
	protected void enableDisableElements() {
		jtfPrefix.setEnabled(jrbOutputJarFixes.isSelected());
		jtfSuffix.setEnabled(jrbOutputJarFixes.isSelected());
		jcbTimestampServerUrl.setEnabled(jcbAddTimestamp.isSelected());
	}

	/**
	 * Convert the supplied signature name to make it valid for use with signing,
	 * i.e. any characters that are not 'a-z', 'A-Z', '0-9', '_' or '-' are
	 * converted to '_'
	 * 
	 * @param signatureName String
	 * @return String
	 */
	private String convertSignatureName(String signatureName) {
		StringBuilder sb = new StringBuilder(signatureName.length());

		for (int i = 0; i < signatureName.length(); i++) {
			char c = signatureName.charAt(i);

			if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' && c != '_') {
				c = '_';
			}
			sb.append(c);
		}

		return sb.toString();
	}

	/**
	 * Populate combination box with items
	 */
	private void populateDigestAlgs() {
		jcbDigestAlgorithm.removeAllItems();

		jcbDigestAlgorithm.addItem(DigestType.SHA1);
		jcbDigestAlgorithm.addItem(DigestType.SHA224);
		jcbDigestAlgorithm.addItem(DigestType.SHA256);
		jcbDigestAlgorithm.addItem(DigestType.SHA384);
		jcbDigestAlgorithm.addItem(DigestType.SHA512);

		jcbDigestAlgorithm.setSelectedItem(DigestType.SHA256);
	}

	/**
	 * Get chosen input JAR file.
	 *
	 * @return <b>File[]</b> input JAR file
	 */
	public File[] getInputJar() {
		return inputJarFiles;
	}

	/**
	 * Get chosen output JAR file.
	 *
	 * @return <b>List<File></b> output JAR file
	 */
	public List<File> getOutputJar() {
		return outputJarFiles;
	}

	/**
	 * Get chosen signature name.
	 *
	 * @return <b>String</b> Signature name or null if dialog cancelled
	 */
	public String getSignatureName() {
		return signatureName;
	}

	/**
	 * Get chosen signature type.
	 *
	 * @return <b>SignatureType</b> or null if dialog cancelled
	 */
	public SignatureType getSignatureType() {
		return signatureType;
	}

	/**
	 * Get chosen digest type.
	 *
	 * @return <b>DigestType</b> or null if dialog cancelled
	 */
	public DigestType getDigestType() {
		return digestType;
	}

	/**
	 * Get chosen TSA URL.
	 *
	 * @return <b>String</b> TSA URL or null if dialog cancelled
	 */
	public String getTimestampingServerUrl() {
		return tsaUrl;
	}

	/**
	 * Verify that the supplied signature name is valid for use in the signing of a
	 * JAR file, ie: contains only alphanumeric characters and the characters '-' or
	 * '_'
	 * 
	 * @param signatureName String
	 * @return <b>Boolean</b>
	 */
	private boolean verifySignatureName(String signatureName) {
		for (int i = 0; i < signatureName.length(); i++) {
			char c = signatureName.charAt(i);

			if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' && c != '_') {
				return false;
			}
		}

		return true;
	}

	/**
	 * The function checks the following
	 * <p>
	 * - dialog fields to ensure they are not empty
	 * <p>
	 * - output file paths for overwriting files
	 * <p>
	 * - signature for overwriting signatures
	 */
	private void okPressed() {
		String signatureName = jtfSignatureName.getText().trim();
		String outputJarPrefix = jtfPrefix.getText().trim();
		String outputJarSuffix = jtfSuffix.getText().trim();

		// check if any files selected
		if ((inputJarFiles == null) || (inputJarFiles.length == 0)) {
			JOptionPane.showMessageDialog(this, res.getString("DSignJar.InputJarRequired.message"), getTitle(),
					JOptionPane.WARNING_MESSAGE);
			return;
		}

		// check if signature field was filled
		if (signatureName.length() == 0) {
			JOptionPane.showMessageDialog(this, res.getString("DSignJar.ValReqSignatureName.message"), getTitle(),
					JOptionPane.WARNING_MESSAGE);
			return;
		}

		// check if signature is verified
		if (!verifySignatureName(signatureName)) {
			JOptionPane.showMessageDialog(this, res.getString("DSignJar.ValJarSignatureName.message"), getTitle(),
					JOptionPane.WARNING_MESSAGE);
			return;
		}

		// check if time stamp URL is empty
		if (jcbAddTimestamp.isSelected() && jcbTimestampServerUrl.getSelectedItem().toString().isEmpty()) {
			JOptionPane.showMessageDialog(this, res.getString("DSignJar.EmptyTimestampUrl.message"), getTitle(),
					JOptionPane.WARNING_MESSAGE);
			return;
		}

		// checks if file prefix or suffix fields were filled
		if (jrbOutputJarFixes.isSelected()) {
			if ((outputJarPrefix.length() == 0) && (outputJarSuffix.length() == 0)) {
				JOptionPane.showMessageDialog(this, res.getString("DSignJar.OutputJarRequired.message"), getTitle(),
						JOptionPane.WARNING_MESSAGE);
				return;
			}
		}

		this.signatureName = signatureName;
		signatureType = (SignatureType) jcbSignatureAlgorithm.getSelectedItem();
		digestType = (DigestType) jcbDigestAlgorithm.getSelectedItem();

		// check add time stamp is selected and assign value
		if (jcbAddTimestamp.isSelected()) {
			tsaUrl = jcbTimestampServerUrl.getSelectedItem().toString();
		}

		// set output Jar files and Overwrite File if selected
		if (!setOutputJarFiles(inputJarFiles)) {
			return;
		}

		// check signature and Overwrite Signature if selected
		if (!checkSignature(inputJarFiles)) {
			return;
		}

		closeDialog();
	}

	/**
	 * Set output JAR files and check files for overwrite
	 * 
	 * @return <b>Boolean</b> true if successful false if no option chosen
	 */
	private boolean setOutputJarFiles(File[] files) {
		String outputJarPrefix = jtfPrefix.getText().trim();
		String outputJarSuffix = jtfSuffix.getText().trim();
		final String FILE_SUFFIX = ".jar";
		JCheckBox checkbox = new JCheckBox(res.getString("DSignJar.OverwriteSkip.message"));

		// set input files array to output files list
		this.outputJarFiles = new ArrayList<File>(Arrays.asList(files));

		if (jrbOutputJarFixes.isSelected()) {
			// loop through output JAR files
			for (int i = 0; i < outputJarFiles.size(); i++) {
				// set prefix and suffix to the file name
				String fileBaseName = FileNameUtil.removeExtension(outputJarFiles.get(i).getName());
				String outFileName = outputJarFiles.get(i).getParent() + "\\" + outputJarPrefix + fileBaseName
						+ outputJarSuffix + FILE_SUFFIX;
				// replace file object in arraylist
				this.outputJarFiles.set(i, new File(outFileName));

				if (!checkbox.isSelected()) {
					// check if file exists
					if (outputJarFiles.get(i).isFile()) {
						String message = MessageFormat.format(res.getString("DSignJar.OverWriteOutputJarFile.message"),
								outputJarFiles.get(i));
						Object[] params = { message, checkbox };

						// check if overwrite is allowed and present checkbox to skip overwrite message
						int selected = JOptionPane.showConfirmDialog(this, params, getTitle(),
								JOptionPane.YES_NO_OPTION);
						if (selected != JOptionPane.YES_OPTION) {
							this.outputJarFiles.clear();
							return false;
						}
					}
				}
			}
		}
		return true;
	}

	/**
	 * Checks to overwrite an existing signature
	 * 
	 * @return <b>Boolean</b> continues jar signing if true cancels process if false
	 */
	private boolean checkSignature(File[] files) {
		JCheckBox checkbox = new JCheckBox(res.getString("DSignJar.OverwriteSkip.message"));
		String message = MessageFormat.format(res.getString("DSignJar.SignatureOverwrite.message"), this.signatureName);
		Object[] params = { message, checkbox };

		for (int i = 0; i < files.length; i++) {
			try {
				// check if the existing signature matches the current signature
				if (JarSigner.hasSignature(files[i], this.signatureName)) {
					// check if overwrite is allowed and present checkbox to skip overwrite message
					int selected = JOptionPane.showConfirmDialog(this, params, getTitle(), JOptionPane.YES_NO_OPTION);
					if (selected != JOptionPane.YES_OPTION) {
						return false;
					}
				}
			} catch (IOException ex) {
				DError.displayError(this, ex);
				return false;
			}
			// check to skip overwrite alert message
			if (checkbox.isSelected()) {
				return true;
			}
		}
		return true;
	}

	/**
	 * Get input JAR files
	 */
	private void inputJarBrowsePressed() {
		JFileChooser chooser = FileChooserFactory.getArchiveFileChooser();
		chooser.setCurrentDirectory(CurrentDirectory.get());
		chooser.setDialogTitle(res.getString("DSignJar.ChooseInputJar.Title"));
		chooser.setMultiSelectionEnabled(true);
		chooser.setApproveButtonText(res.getString("DSignJar.InputJarChooser.button"));

		int rtnValue = chooser.showOpenDialog(this);
		if (rtnValue == JFileChooser.APPROVE_OPTION) {
			File[] chosenFiles = chooser.getSelectedFiles();

			// check if selected are JAR files
			for (File file : chosenFiles) {
				if (!validJAR(file)) {
					return;
				}
			}
			CurrentDirectory.updateForFile(chosenFiles[0]);
			// set input files from file selector
			this.inputJarFiles = chooser.getSelectedFiles();
			// update file count label
			updateFileCount(chosenFiles.length);
		}
	}
	
	private void updateFileCount(int fileCount) {
		// set message string
		String message = MessageFormat.format(res.getString("DSignJar.jlFileCount.text"), fileCount);
		jlFileCount.setText(message);
	}

	/**
	 * Check if a file is a valid JAR
	 * 
	 * @param File accepts a jar file
	 * @return <b>Boolean</b> true if the file is a valid jar and false if not
	 */
	private boolean validJAR(File file) {
		JarFile jarFile = null;

		try {
			jarFile = new JarFile(file);
		} catch (IOException ex) {
			String problemStr = MessageFormat.format(res.getString("DSignJar.NoOpenJar.Problem"), file.getName());

			String[] causes = new String[] { res.getString("DSignJar.NotJar.Cause"),
					res.getString("DSignJar.CorruptedJar.Cause") };

			Problem problem = new Problem(problemStr, causes, ex);

			DProblem dProblem = new DProblem(this, res.getString("DSignJar.ProblemOpeningJar.Title"), problem);
			dProblem.setLocationRelativeTo(this);
			dProblem.setVisible(true);

			return false;
		} finally {
			IOUtils.closeQuietly(jarFile);
		}

		return true;
	}

	/**
	 * Returns the current success status
	 *
	 * @return successStatus true if successful false if not
	 */
	public boolean isSuccessful() {
		return successStatus;
	}

	/**
	 * Call the close dialog method
	 */
	private void cancelPressed() {
		successStatus = false;
		closeDialog();
	}

	/**
	 * Close the dialog method
	 */
	private void closeDialog() {
		setVisible(false);
		dispose();
	}

	// for quick UI testing
	public static void main(String[] args) throws Exception {
		DialogViewer.prepare();
		KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyPairType.RSA.jce(), "BC");
		kpg.initialize(1024, new SecureRandom());
		KeyPair kp = kpg.generateKeyPair();

		DSignJar dialog = new DSignJar(new JFrame(), kp.getPrivate(), KeyPairType.RSA, "signature name");
		DialogViewer.run(dialog);
	}
}